#pragma once

#include <memory>
#include <vector>

#include <gnuradio/domain_adapter.hpp>
#include <gnuradio/graph.hpp>

namespace gr {


// Domain configuration
class domain_adapter_conf
{
protected:
    domain_adapter_conf(buffer_preference_t buf_pref) : _buf_pref(buf_pref) {}
    buffer_preference_t _buf_pref;

public:
    virtual std::pair<domain_adapter_sptr, domain_adapter_sptr>
    make_domain_adapter_pair(port_sptr upstream_port, port_sptr downstream_port)
    {
        throw std::runtime_error("Cannot create domain adapter pair from base class");
    };
};

typedef std::shared_ptr<domain_adapter_conf> domain_adapter_conf_sptr;

/**
 * @brief Domain Adapter configuration for specified endpoint pair
 *
 * Allows a domain adapter pair to be generated by specifying a pair of endpoints
 */
class domain_adapter_zmq_conf : public domain_adapter_conf
{
public:
    typedef std::shared_ptr<domain_adapter_zmq_conf> sptr;
    static sptr make(buffer_preference_t buf_pref,
                     const std::string& endpoint_upstream,
                     const std::string& endpoint_downstream)
    {
        return std::make_shared<domain_adapter_zmq_conf>(
            domain_adapter_zmq_conf(buf_pref, endpoint_upstream, endpoint_downstream));
    }

    domain_adapter_zmq_conf(buffer_preference_t buf_pref,
                            const std::string& endpoint_upstream,
                            const std::string& endpoint_downstream)
        : domain_adapter_conf(buf_pref),
          _endpoint_upstream(endpoint_upstream),
          _endpoint_downstream(endpoint_downstream)
    {
    }

    virtual std::pair<domain_adapter_sptr, domain_adapter_sptr>
    make_domain_adapter_pair(port_sptr upstream_port, port_sptr downstream_port)
    {

        if (_buf_pref == buffer_preference_t::DOWNSTREAM) {
            auto upstream_adapter =
                domain_adapter_zmq_req_cli::make(_endpoint_upstream, upstream_port);
            auto downstream_adapter =
                domain_adapter_zmq_rep_svr::make(_endpoint_downstream, downstream_port);
            return std::make_pair(upstream_adapter, downstream_adapter);
        } else {
            auto downstream_adapter =
                domain_adapter_zmq_req_cli::make(_endpoint_downstream, downstream_port);
            auto upstream_adapter =
                domain_adapter_zmq_rep_svr::make(_endpoint_upstream, upstream_port);
            return std::make_pair(upstream_adapter, downstream_adapter);
        }
    }

private:
    std::string _endpoint_upstream = "";
    std::string _endpoint_downstream = "";
};

// typedef std::map<edge, domain_adapter_conf> domain_adapter_conf_per_edge;
typedef std::vector<std::tuple<edge, domain_adapter_conf_sptr>>
    domain_adapter_conf_per_edge;


// Default Domain Adapter configuration should be derived from some sort of preferences
// file
// TODO: preferences

class domain_adapter_zmq_tcp_conf : public domain_adapter_conf
{
public:
    typedef std::shared_ptr<domain_adapter_zmq_tcp_conf> sptr;
    static sptr make(std::vector<int> ports,
                     std::string ip_address = "127.0.0.1",
                     buffer_preference_t buf_pref = buffer_preference_t::DOWNSTREAM)
    {
        return std::make_shared<domain_adapter_zmq_tcp_conf>(
            domain_adapter_zmq_tcp_conf(ports, ip_address, buf_pref));
    }

    domain_adapter_zmq_tcp_conf(std::vector<int> ports,
                                std::string ip_address,
                                buffer_preference_t buf_pref)
        : domain_adapter_conf(buf_pref), _available_ports(ports), _ip_address(ip_address)
    {
    }

    virtual std::pair<domain_adapter_sptr, domain_adapter_sptr>
    make_domain_adapter_pair(port_sptr upstream_port, port_sptr downstream_port)
    {
        // TODO: check whether port is available
        auto port = _available_ports[0];
        _available_ports.erase(_available_ports.begin());

        std::string endpoint_upstream =
            "tcp://" + _ip_address + ":" + std::to_string(port);
        std::string endpoint_downstream =
            "tcp://" + _ip_address + ":" + std::to_string(port);
        if (_buf_pref == buffer_preference_t::DOWNSTREAM) {
            auto upstream_adapter =
                domain_adapter_zmq_req_cli::make(endpoint_upstream, upstream_port);
            auto downstream_adapter =
                domain_adapter_zmq_rep_svr::make(endpoint_downstream, downstream_port);

            return std::make_pair(upstream_adapter, downstream_adapter);
        } else {
            auto downstream_adapter =
                domain_adapter_zmq_req_cli::make(endpoint_downstream, upstream_port);
            auto upstream_adapter =
                domain_adapter_zmq_rep_svr::make(endpoint_upstream, downstream_port);

            return std::make_pair(upstream_adapter, downstream_adapter);
        }
    }

private:
    std::vector<int> _available_ports;
    std::string _ip_address;
    std::string _endpoint_upstream = "";
    std::string _endpoint_downstream = "";
};


} // namespace gr